﻿/*
   Open Intel 
   Copyright © 2011 – ISC. All Rights Reserved.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Caliburn.Micro;
using ISC.MapDotNetServer.Common.Data;
using OI.Async;
using OI.Controls;
using ISC.MapDotNetServer.Common.Maps;
using ISC.RIM.Silverlight;
using OI.Events;
using Action = System.Action;
using Map = ISC.MapDotNetServer.Common.Maps.Map;
using RIMMap = ISC.RIM.Silverlight.Map;
using Shape = ISC.MapDotNetServer.Common.Shape;
using WINPoint = System.Windows.Point;

namespace OI.Framework
{
    [Export(typeof(DrillDownManager))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class DrillDownManager :
        IHandle<SelectMap>,
        IHandle<DisplayRIMMap>,
        IHandle<MouseLeftClickOnMap>,
        IHandle<MouseHoverOverMap>,
        IHandle<QueriedResultSets>,
        IHandle<EnableDrillDown>
    {
        #region DATA
        readonly IEventAggregator Events;
        public Map Map { get; private set; }
        public RIMMap RIMMap { get; private set; }
        public WINPoint CurrentQueryPoint;
        public DrillDownCallout CurrentCallout;
        public List<IMapViewChild> CurrentHighlights = new List<IMapViewChild>();
        public bool Enabled { get; set; }

        private double _CalloutWidth = 360;
        public double CalloutWidth
        {
            get { return _CalloutWidth; }
            set { _CalloutWidth = value; }
        }

        private double _CalloutHeight = 240;
        public double CalloutHeight
        {
            get { return _CalloutHeight; }
            set { _CalloutHeight = value; }
        }

        private double _CalloutFontSize = 11;
        public double CalloutFontSize
        {
            get { return _CalloutFontSize; }
            set { _CalloutFontSize = value; }
        }

        private double _PhotoSize = 64;
        public double PhotoSize
        {
            get { return _PhotoSize; }
            set { _PhotoSize = value; }
        }

        #endregion


        [ImportingConstructor]
        public DrillDownManager(IEventAggregator events)
        {
            Events = events;
            events.Subscribe(this);
        }


        /// <summary>
        /// this registers a callout action with this Callout manager
        /// </summary>
        /// <param name="tableName">table name to associate this action with</param>
        /// <param name="action">takes the table name and current result row</param>
        public void RegisterCalloutAction(string tableName, CalloutAction action)
        {
            if (CalloutAction.CalloutActions.ContainsKey(tableName))
            {
                CalloutAction.CalloutActions[tableName] = action;
            }
            else
            {
                CalloutAction.CalloutActions.Add(tableName, action);
            }
        }


        // when a new map is launched, the drill-down manager is notified
        public void Handle(SelectMap message)
        {
            Map = message.Map;
        }


        // handle when a RIM Map is ready
        // grab a reference here since most toolbox items must manipulate the map
        public void Handle(DisplayRIMMap message)
        {
            RIMMap = message.Map;
        }


        // handle a mouse hover event over a map
        public void Handle(MouseHoverOverMap message)
        {
            if (Enabled)
            {
                Start(message.Location.MapPoint);
                message.Location.Handled = true;
            }
        }


        // handle a mouse left-click event over a map
        public void Handle(MouseLeftClickOnMap message)
        {
            if (Enabled)
            {
                Start(message.Location.MapPoint);
                message.Location.Handled = true;
            }
        }


        // enable / disable drilldown
        public void Handle(EnableDrillDown message)
        {
            Enabled = message.Enable;
            if (!Enabled)
            {
                CloseCallout();
                ClearHighlights();
            }
            if (message.CalloutHeight > 0)
            {
                CalloutHeight = message.CalloutHeight;
            }
            if (message.CalloutWidth > 0)
            {
                CalloutWidth = message.CalloutWidth;
            }
        }


        // main entry point into a drill-down query
        public void Start(WINPoint clickPt)
        {
            if (CurrentQueryPoint != clickPt && RIMMap != null)
            {
                CurrentQueryPoint = clickPt;
                var spatialFilter = clickPt.ToMDNSPoint();

                // create a drill-down spatial filter
                // since this is a multi-layer drill-down with potentially different feature classes, use a circular polygon
                double bufferDist = (RIMMap.MapView.GetMapExtents().MagX / RIMMap.MapView.ActualWidth) *
                    Configuration.DrillDownPixelRadius;

                Coroutine.BeginExecute(QueryIterator(spatialFilter, bufferDist));
            }
        }


        // coroutine for async query
        public IEnumerator<IResult> QueryIterator(Shape spatialFilter, double bufferDist)
        {
            // serialize spatial filter
            string filterStr = spatialFilter.Serialize();

            // loop multiple layers to query (drill-down)
            if (!string.IsNullOrEmpty(filterStr))
            {
                // current scale
                double scale = RIMMap.MapView.GetProjectionMapScale();

                // note: currently, all visible and in-scale layers that are not ignored are queried
                var layers = Map.Layers.Where(layer =>
                    layer.Visible &&
                   (Math.Abs(layer.GetMinScale() - -1) < Double.Epsilon || scale >= layer.GetMinScale()) &&
                   (Math.Abs(layer.GetMaxScale() - -1) < Double.Epsilon || scale <= layer.GetMaxScale()) &&
                   !layer.DrilldownIgnore()
                   ).ToList();

                if (layers.Count > 0)
                {
                    var dsq = new MDNDrillDownQuery(layers, filterStr, bufferDist);
                    yield return dsq;

                    // broadcast the arrival of query results
                    Events.Publish(new QueriedResultSets
                    {
                        Sources = layers,
                        Tables = dsq.Results,
                        ShowInCallout = true
                    });
                }
            }
        }


        // handle a query results received event
        // present callout and highlight shapes
        public void Handle(QueriedResultSets message)
        {
            // if we are to show the data in a callout
            if (message.ShowInCallout &&
                RIMMap != null &&
                RIMMap.MapController != null)
            {
                // the callout body
                var container = new ItemsControl
                {
                    Style = Application.Current.Resources["VirtualItemsControl"] as Style,
                };

                bool thereWasData = false;

                // remove previous content
                CloseCallout();
                ClearHighlights();

                // iterate through potentially multiple resultsets (drill-down)
                int idx = 0;
                foreach (var table in message.Tables)
                {
                    // skip empty resultsets
                    if (table.QueryResultRows.Count == 0)
                    {
                        continue;
                    }
                    thereWasData = true;

                    // shows on tab
                    string tableName = table.Name;

                    // find the associated map Layer
                    Layer mapLayer = message.Sources[idx++];

                    // get friendly name for tab
                    string tableNameScrubbed = mapLayer.GetLayerName(tableName);

                    // if a column holds a uri to an image
                    string photoUriColumn = mapLayer.GetDrilldownPhotoColumn();

                    // get headings
                    var headings = mapLayer.HasDrilldownHeaders() ?
                        new List<string>(mapLayer.GetDrilldownHeaders().Split(',')) :
                        table.Fields;

                    // title
                    string title = tableNameScrubbed +
                        (table.QueryResultRows.Count > 1 ?
                        " (" + table.QueryResultRows.Count + ")" : "");

                    container.Items.Add(GetTitleElement(title));

                    // iterate rows
                    int rowIdx = 0;
                    foreach (var row in table.QueryResultRows)
                    {
                        var hs = MapShapes.CreateHighlightShape(row.Shape);
                        CurrentHighlights.Add(hs);
                        RIMMap.MapController.AddMapViewChild(hs);

                        // get a callout action if any and add it to the callout
                        if (CalloutAction.CalloutActions.ContainsKey(tableName))
                        {
                            var r = row;
                            var calloutAction = CalloutAction.CalloutActions[tableName];
                            var hl = new HyperlinkButton
                            {
                                Content = calloutAction.CalloutActionText,
                                Margin = new Thickness(0, -10, 0, 10),
                                HorizontalAlignment = HorizontalAlignment.Left
                            };

                            hl.Click += (s, e) => calloutAction.CalloutActionRowHandler(tableName, r);
                            container.Items.Add(hl);
                        }

                        // iterate rows and add to callout (header, value) pairs
                        for (int i = 0; i < table.QueryResultRows[rowIdx].Values.Count; i++)
                        {
                            // get the field
                            string field = i < headings.Count ? headings[i] : table.Fields[i];

                            // get value
                            string val = table.QueryResultRows[rowIdx].Values[i];

                            // if an image
                            if (!string.IsNullOrEmpty(photoUriColumn) &&
                                table.Fields[i] == photoUriColumn &&
                                !string.IsNullOrEmpty(val))
                            {
                                container.Items.Add(GetImageElement(val));
                            }
                            // this is text
                            else
                            {
                                // get type
                                string t = (table.HasTypes ? table.TypeNames[i] : null);

                                // skip over empty text
                                if (t == "System.String" && string.IsNullOrEmpty(val))
                                {
                                    continue;
                                }
                                container.Items.Add(GetFieldElement(t, field, val, (rowIdx > 0 && i == 0)));
                            }

                        } // end of data for this row

                        // spacer
                        var rect = new Rectangle
                        {
                            Height = 3,
                            Width = CalloutWidth * 0.8,
                            Fill = new SolidColorBrush(Color.FromArgb(64, 0, 0, 0)),
                            Margin = new Thickness(0, 8, 0, 8),
                            HorizontalAlignment = HorizontalAlignment.Left
                        };
                        container.Items.Add(rect);

                        rowIdx++;
                    } // end of rows for this table

                    // spacer
                    container.Items.Add(GetTitleElement(""));

                } // end of all tables

                if (thereWasData)
                {
                    Action closeAct = () =>
                    {
                        ClearHighlights();
                        RIMMap.MapController.RemoveMapViewChild(CurrentCallout);
                    };

                    CurrentCallout = new DrillDownCallout(CurrentQueryPoint, CalloutWidth, CalloutHeight, container, closeAct);
                    RIMMap.MapController.AddMapViewChild(
                        CurrentCallout,
                        201);

                }
            }
        }


        public void CloseCallout()
        {
            if (CurrentCallout != null)
            {
                CurrentCallout.Close();
                CurrentCallout = null;
            }
        }


        public void ClearHighlights()
        {
            if (RIMMap != null && RIMMap.MapController != null)
            {
                foreach (var ch in CurrentHighlights)
                {
                    RIMMap.MapController.RemoveMapViewChild(ch);
                }
            }
            CurrentHighlights.Clear();
        }


        /// <summary>
        /// Gets the title element.
        /// </summary>
        /// <param name="title">The title.</param>
        /// <returns>A TextBox with the title.</returns>
        private UIElement GetTitleElement(string title)
        {
            var tb = new TextBlock
            {
                Text = title,
                Style = (Style)Application.Current.Resources["CalloutHeaderText"],
                FontSize = CalloutFontSize * 2.0
            };
            return tb;
        }


        /// <summary>
        /// Gets the field element.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="label">The label.</param>
        /// <param name="valueStr">The value.</param>
        /// <param name="addSpaceAbove">if set to <c>true</c> [add space above].</param>
        /// <returns>A Grid with a label/value pair.</returns>
        private UIElement GetFieldElement(string type, string label, string valueStr, bool addSpaceAbove)
        {
            var grid = new Grid();

            var cd = new ColumnDefinition { Width = new GridLength(CalloutWidth * 0.35, GridUnitType.Pixel) };
            grid.ColumnDefinitions.Add(cd);

            cd = new ColumnDefinition { Width = new GridLength(CalloutWidth * 0.50, GridUnitType.Pixel) };
            grid.ColumnDefinitions.Add(cd);

            grid.Margin = new Thickness(0, (addSpaceAbove ? 8 : 0), 0, 2);

            // clean up numbers
            if (!string.IsNullOrEmpty(type) &&
                (type == "System.Double" ||
                 type == "System.Decimal" ||
                 type == "System.Single") &&
                 valueStr.IndexOf('.') > 0)
            {
                valueStr = valueStr.TrimEnd('0');
                if (valueStr.EndsWith("."))
                {
                    valueStr = valueStr.TrimEnd('.');
                }
            }

            var tbLab = new TextBlock
            {
                Text = label,
                Style = (Style)Application.Current.Resources["CalloutFieldText"],
                FontSize = CalloutFontSize,
                Margin = new Thickness(2, 0, 0, 0)
            };

            grid.Children.Add(tbLab);

            var tb = new TextBlock
            {
                Text = valueStr,
                Style = (Style)Application.Current.Resources["CalloutValueText"],
                FontSize = CalloutFontSize,
            };

            // if this is a hyperlink
            Uri uri;
            if (Uri.TryCreate(valueStr, UriKind.Absolute, out uri))
            {
                var hl = new HyperlinkButton
                {
                    Content = valueStr,
                    NavigateUri = uri,
                    TargetName = "_blank",
                };
                Grid.SetColumn(hl, 1);
                grid.Children.Add(hl);
            }
            else
            {
                Grid.SetColumn(tb, 1);
                grid.Children.Add(tb);
            }

            return grid;
        }


        /// <summary>
        /// Gets the image element.
        /// </summary>
        /// <param name="uriStr">The URI STR.</param>
        /// <returns></returns>
        private UIElement GetImageElement(string uriStr)
        {
            try
            {
                var uri = new Uri(uriStr);
                var grid = new Grid();

                var cd = new ColumnDefinition { Width = new GridLength(CalloutWidth * 0.35, GridUnitType.Pixel) };
                grid.ColumnDefinitions.Add(cd);

                cd = new ColumnDefinition { Width = new GridLength(CalloutWidth * 0.50, GridUnitType.Pixel) };
                grid.ColumnDefinitions.Add(cd);

                var tb = new TextBlock
                {
                    Text = "Photo:",
                    Style = (Style)Application.Current.Resources["CalloutFieldText"],
                    FontSize = CalloutFontSize,
                    Margin = new Thickness(2, 0, 0, 0),
                    VerticalAlignment = VerticalAlignment.Top,
                    TextWrapping = TextWrapping.Wrap
                };
                grid.Children.Add(tb);

                var bi = new BitmapImage(uri);
                var image = new Image
                {
                    Stretch = Stretch.UniformToFill,
                    Width = PhotoSize,
                    Height = PhotoSize,
                    HorizontalAlignment = HorizontalAlignment.Left,
                    Source = bi
                };
                image.ImageFailed += (s, e) =>
                {
                    tb.Visibility = Visibility.Collapsed;
                    image.Visibility = Visibility.Collapsed;
                };

                image.MouseLeftButtonUp += (s, e) => Events.Publish(new PopupImage
                {
                    Image = bi,
                    ImageUri = uri
                });

                ToolTipService.SetToolTip(image, "click to enlarge");

                Grid.SetColumn(image, 1);
                grid.Children.Add(image);

                return grid;
            }
            catch
            { }
            return null;
        }
    }


    public class CalloutAction
    {
        // this is an optional action you can set on the drill-down manager
        // it takes a table name and QueryResultRow that is processed when the user fires the action
        public Action<string, QueryResultRow> CalloutActionRowHandler { get; private set; }

        // the callout action text displayed in the callout
        public string CalloutActionText { get; private set; }


        public CalloutAction(string calloutActionText, Action<string, QueryResultRow> calloutActionRowHandler)
        {
            CalloutActionText = calloutActionText;
            CalloutActionRowHandler = calloutActionRowHandler;
        }

        public static Dictionary<string, CalloutAction> CalloutActions = new Dictionary<string, CalloutAction>();
    }
}
